﻿// Spun AJAX Framework
// Copyright (C) 2008 Blake Hegerle
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Reflection;
using System.Text.RegularExpressions;

namespace Spun.Models.Prototyping
{
    public sealed class CodeGenerator<T>
        where T : class
    {
        public const string GENERATED_CODE_NAMESPACE = "Spun.Models.Prototyping.Generated";
        public const string GENERATED_CODE_TYPE_PREFIX = "_Generated_";
        public const string BASE_CLASS_GET_METHOD = "Get";
        public const string BASE_CLASS_SET_METHOD = "Update";

        public CodeGenerator()
        {
            TypeChecker<T>.Check();

            referencedTypes = new List<Type>();
            referencedTypes.Add(typeof(T));
        }

        public IEnumerable<Type> ReferencedTypes
        {
            get { return referencedTypes; }
        }

        public string GeneratedTypeName
        {
            get { return Namespace + '.' + TypeName; }
        }

        public CodeNamespace Code
        {
            get
            {
                var NamespaceDecl = new CodeNamespace(Namespace);

                NamespaceDecl.Types.Add(TypeDecl);

                return NamespaceDecl;
            }
        }

        private CodeTypeDeclaration TypeDecl
        {
            get
            {
                var TypeDecl = new CodeTypeDeclaration(TypeName);

                TypeDecl.BaseTypes.Add(BaseClass);
                TypeDecl.BaseTypes.Add(Interface);

                AddProperties(TypeDecl);

                AddStaticFields(TypeDecl);

                AddStaticConstructor(TypeDecl);

                AddInstanceContructor(TypeDecl);

                return TypeDecl;
            }
        }

        private void AddInstanceContructor(CodeTypeDeclaration TypeDecl)
        {
            var Constructor = new CodeConstructor();

            Constructor.Attributes = MemberAttributes.Public;

            Constructor.BaseConstructorArgs.Add(new CodePrimitiveExpression(typeof(T).FullName));
            Constructor.BaseConstructorArgs.Add(new CodeVariableReferenceExpression("Members"));

            TypeDecl.Members.Add(Constructor);
        }

        private void AddStaticConstructor(CodeTypeDeclaration TypeDecl)
        {
            var ListName = "List";
            var ListType = new CodeTypeReference(typeof(List<ModelMember>));

            // Declare the list
            var ListDeclStatement = new CodeVariableDeclarationStatement(ListType, ListName);

            var ListRef = new CodeVariableReferenceExpression(ListName);

            // Make a new List<ModelMember>
            var ListConstructionStatement = new CodeAssignStatement();
            ListConstructionStatement.Left = ListRef;
            ListConstructionStatement.Right = new CodeObjectCreateExpression(ListType);

            var Adds = new List<CodeStatement>();
            foreach (var Property in PropertyEnumerator.Properties(typeof(T)))
            {
                var Create = new CodeObjectCreateExpression(new CodeTypeReference(typeof(ModelMember)));

                Create.Parameters.Add(new CodePrimitiveExpression(Property.Name));
                Create.Parameters.Add(new CodeTypeOfExpression(new CodeTypeReference(Property.PropertyType)));

                var AddMethodInvocation = new CodeMethodInvokeExpression(ListRef, "Add", Create);

                Adds.Add(new CodeExpressionStatement(AddMethodInvocation));
            }

            var AssignMemberStatement = new CodeAssignStatement(new CodeVariableReferenceExpression("Members"), ListRef);

            // Make the static constructor
            var Constructor = new CodeTypeConstructor();
            Constructor.Statements.Add(ListDeclStatement);
            Constructor.Statements.Add(ListConstructionStatement);
            Constructor.Statements.AddRange(Adds.ToArray());
            Constructor.Statements.Add(AssignMemberStatement);

            // Add the static constructor to the class
            TypeDecl.Members.Add(Constructor);
        }

        private void AddStaticFields(CodeTypeDeclaration TypeDecl)
        {
            var ModelListType = new CodeTypeReference(typeof(IEnumerable<ModelMember>));

            var ModelMemberListField = new CodeMemberField(ModelListType, "Members");
            ModelMemberListField.Attributes = MemberAttributes.Static | MemberAttributes.Private;

            TypeDecl.Members.Add(ModelMemberListField);
        }

        private CodeTypeReference Interface
        {
            get { return new CodeTypeReference(typeof(T)); }
        }

        private CodeTypeReference BaseClass
        {
            get { return GetTypeRef(typeof(ProxyModel<T>)); }
        }

        private string TypeName
        {
            get { return GENERATED_CODE_TYPE_PREFIX + SafeName; }
        }

        private string SafeName
        {
            get
            {
                return Regex.Replace(typeof(T).FullName, "[^A-Za-z]", "_");
            }
        }

        private string Namespace
        {
            get { return GENERATED_CODE_NAMESPACE; }
        }

        private void AddProperties(CodeTypeDeclaration TypeDecl)
        {
            foreach (var InterfaceProperty in PropertyEnumerator.Properties(typeof(T)))
            {
                CheckSupportedProperty(InterfaceProperty);

                var Property = new CodeMemberProperty();

                Property.Name = InterfaceProperty.Name;
                Property.Type = GetTypeRef(InterfaceProperty.PropertyType);
                Property.HasGet = Property.HasSet = true;
                Property.Attributes = MemberAttributes.Public;

                AddSet(InterfaceProperty.Name, Property);
                AddGet(InterfaceProperty.Name, Property);

                TypeDecl.Members.Add(Property);
            }
        }

        private void CheckSupportedProperty(PropertyInfo Property)
        {
            if (!Property.CanRead)
            {
                throw new UnsupportedInterface<T>("write-only property");
            }
            else
            {
                if (!Property.CanWrite)
                    throw new UnsupportedInterface<T>("read-only property");
            }
        }

        private void AddGet(string Name, CodeMemberProperty Property)
        {
            var Method = new CodeMethodReferenceExpression();
            Method.TypeArguments.Add(Property.Type);
            Method.MethodName = BASE_CLASS_GET_METHOD;

            var Invocation = new CodeMethodInvokeExpression();
            Invocation.Method = Method;
            Invocation.Parameters.Add(new CodePrimitiveExpression(Property.Name));

            var Return = new CodeMethodReturnStatement();
            Return.Expression = Invocation;
            Property.GetStatements.Add(Return);
        }

        private void AddSet(string Name, CodeMemberProperty Property)
        {
            var Method = new CodeMethodReferenceExpression();
            Method.MethodName = BASE_CLASS_SET_METHOD;

            var Invocation = new CodeMethodInvokeExpression();
            Invocation.Method = Method;
            Invocation.Parameters.Add(new CodePrimitiveExpression(Property.Name));
            Invocation.Parameters.Add(new CodePropertySetValueReferenceExpression());

            Property.SetStatements.Add(Invocation);
        }

        private CodeTypeReference GetTypeRef(Type Type)
        {
            referencedTypes.Add(Type);
            return new CodeTypeReference(Type);
        }

        private List<Type> referencedTypes;
    }
}
